一般在做 API 設計時,都會盡量收斂設計,期望一組 API 不用太多支,每支 API 參數和欄位不要太複雜,希望這些 API 可以像最經典的樂高積木一樣小而巧,使用者可以自由組合 API 積木做出各式各樣的應用。然而現實世界中,總是會遇到種種原因出現錯綜複雜的 API 設計,同一支 API 要收幾十個參數,回傳又有百百種。
在過往經驗裡,通常是因為參數要同時有多個選項,而每個選項又有複數個子選項要一起進行設定,於是條列了大量的參數,又加上許多文字描述各種排列組合,導致文件難以閱讀。延伸前幾日的會員服務為例,會員允許多種登入方式,而透過不同的登入方式會有不同的註冊資訊組合,包含:以使用者名稱和密碼註冊(有使用者名稱和密碼兩個欄位)、以 Email 和密碼註冊(有 Email 和密碼兩個欄位)、和使用三方登入(只有一個外部 ID)。這時若條列註冊會員的參數,會得到這樣的資料結構:
components:
schemas:
Member:
type: object
properties:
memberId:
type: string
account:
type: object
properties:
username:
type: string
description: 使用者名稱,以使用者名稱註冊時必填
email:
type: string
format: email
description: 電子郵件地址,以 Email 註冊時必填
password:
type: string
format: password
description: 密碼,以使用者名稱或 Email 註冊時必填
thirdPartyId:
type: string
description: 第三方服務 ID,以第三方服務註冊時必填
name:
type: string
createdAt:
type: string
format: date-time
可以看到文件中 description
描述了各種場景對應的規則,這時我們可以視需求使用 oneOf
、anyOf
和 allOf
來描述這類多型的結構。首先先說明這三個關鍵字的定義:
oneOf
:必須符合其中一個列出的 schema,且不符合其他的 schema。anyOf
:必須符合其中一個以上列出的 schema。allOf
:必須符合所有列出的 schema。components:
schemas:
A:
type: object
properties:
a:
type: string
somethingElse:
type: string
B:
type: object
properties:
b:
type: string
Example1:
oneOf:
- $ref: '#/components/schemas/A'
- $ref: '#/components/schemas/B'
Example2:
anyOf:
- $ref: '#/components/schemas/A'
- $ref: '#/components/schemas/B'
Example3:
allOf:
- $ref: '#/components/schemas/A'
- $ref: '#/components/schemas/B'
在範例文件中,Example1
結構可以是 {"a":"A"}
或 {"b":"B"}
,但不可以是 {"a":"A", "b":"B"}
;Example2
結構可以是 {"a":"A"}
或 {"b":"B"}
,也可以是 {"a":"A", "b":"B"}
;而 Example3
結構則必須是 {"a":"A", "b":"B"}
,不可以是 {"a":"A"}
也不可以是 {"b":"B"}
。套用到前面的會員註冊範例,我們可以使用 anyOf
將結構改寫如下:
components:
schemas:
UsernameAccount:
type: object
properties:
username:
type: string
password:
type: string
format: password
required:
- username
- password
EmailAccount:
type: object
properties:
email:
type: string
format: email
password:
type: string
format: password
required:
- email
- password
ThirdPartyAccount:
type: object
properties:
thirdPartyId:
type: string
required:
- thirdPartyId
Member:
type: object
properties:
memberId:
type: string
account:
anyof:
- $ref: '#/components/schemas/UsernameAccount'
- $ref: '#/components/schemas/EmailAccount'
- $ref: '#/components/schemas/ThirdPartyAccount'
name:
type: string
createdAt:
type: string
format: date-time
此時在 Redoc 上 account
欄位會呈現出三個結構名稱,點選後會切換顯示:
以上是 OpenAPI 的基本介紹,由於只挑選了部份我認為重要的項目出來分享,更多細節及功能請參考 OpenAPI 官網。
身為懶惰工程師的一員,能少打一個字就少打一個字,手刻 yaml 或 json 寫文件實在太難以忍受!明天,將介紹如何透過 DSL 語言 TypeSpec 減輕撰寫 OpenAPI 文件的負擔。